/* Copyright (c) 2006, geir.tv
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *    * Redistributions of source code must retain the above copyright 
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright 
 *      notice, this list of conditions and the following disclaimer in the 
 *      documentation and/or other materials provided with the distribution.
 *    * Neither the name of geir.tv nor the names of its 
 *      contributors may be used to endorse or promote products derived from 
 *      this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package tv.geir.slackspace.code;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Use this to class to scan classpath for a class' bytecode.
 * 
 * @author Geir Gullestad Pettersen
 * 
 */
class BytecodeClasspathScanner {
	/**
	 * Logger for this class
	 */
	private static Log log = LogFactory.getLog(BytecodeClasspathScanner.class);

	private Set<ClasspathEntry> cpEntries = null;

	/**
	 * Scans classpath for a class with the name <code>className</code> and return that
	 * class' bytecode. If class cannot be found <code>null</code> is returned.
	 * 
	 * @param classname
	 *            class to search for.
	 * @return classdefinition for the class if found, <code>null</code> if no match.
	 */
	public ClassDefinition findClassDefinition(String classname) {
		ClassDefinition cd = null;
		
		try {
			byte[] def = findBytecode(classname);
	
			cd = new ClassDefinition(classname, def);
		} catch( BytecodeRetrievalException bnfe ) {
			if( log.isDebugEnabled() ) {
				log.debug( "Did not find bytecode for \'" + classname + "\' in classpath", bnfe );
			}
		}
		return cd;
	}
	
	public void addClassPathEntry( ClasspathEntry cpe ) {
		getClasspathEntries().add( cpe );
	}

	/**
	 * Returns bytecode for <code>classname</code> if found in CLASSPATH. Same
	 * as using:
	 * 
	 * <pre>
	 *   String classname = &quot;foo.bar.SomeClass&quot;;
	 *   findByteCode( classname, null )
	 * </pre>.
	 * 
	 * @param classname
	 * @return bytecode for the class.
	 * @throws BytecodeRetrievalException
	 *             if byte code was not found or if any error occured during
	 *             search
	 */
	public byte[] findBytecode(String classname)
			throws BytecodeRetrievalException {
		return findBytecode(classname, null);
	}

	/**
	 * Returns bytecode for <code>classname</code> if found. If
	 * <code>path</code> is <code>null</code> then CLASSPATH is scanned for
	 * the specified class. <code>path</code> can however be path to a folder
	 * or a jar-file, in such cases that folder or jar-file will be searched for
	 * the given class.
	 * 
	 * @param classname
	 * @param path
	 *            Specify which path (folder or jar-file) to scan. If
	 *            <code>null</code> then java class path is used as default.
	 * @return bytecode for given class, <code>null</code if bytecode was not found.
	 * @throws BytecodeRetrievalException
	 *             if byte code was not found or if any error occured during
	 *             search
	 */
	public byte[] findBytecode(String classname, File path)
			throws BytecodeRetrievalException {
		if (classname == null) {
			throw new NullPointerException(
					"parameter \'classname\' cannot be null");
		}
		if( log.isDebugEnabled() ) {
			log.debug("fileToByteArray( " + path + ", " + classname + " )");
		}
		
		byte[] fileBytes = new byte[0];

		/* evaluate path and use appropriate search method */
		if (path == null) {
			/* no path given, search entire classpath */
			fileBytes = getByteCodeFromClasspath(classname);
		} else if (path.getPath().endsWith(".jar")) {
			/* search given jar-file */
			fileBytes = getByteCodeFromJarFile( classname, path);
		} else {
			/* search given folder */
			fileBytes = getByteCodeFromFolder( classname, path);
		}

		if (fileBytes == null || fileBytes.length == 0) {
			throw new BytecodeRetrievalException("0 bytes returned, class \""
					+ classname + "\" was not found.");
		}
		return fileBytes;	
	}

	/**
	 * 
	 * @param classname
	 * @param directory
	 *            the directory relative that corresponds to
	 *            <code>classname</code>'s classpath.
	 * @return bytecode for the given class if found in directory, <code>null</code> if not.
	 * @throws BytecodeRetrievalException if any error occurs while reading/accessing the file
	 *             if given directory is invalid or does not exist.
	 * @throws IOException
	 *             if any i/o error occurs while reading file or directory.
	 */
	public byte[] getByteCodeFromFolder(String classname, File directory)
			throws BytecodeRetrievalException {
		byte[] bytecode = null;

		/* construct file-object for directory and classname */
		String folder = directory.getAbsolutePath();
		String classFilePath = classname.replace('.', File.separatorChar);
		StringBuffer filename = new StringBuffer();

		/* add trailing slash if missing */
		filename.append(folder);
		if (!folder.endsWith(File.separator)) {
			filename.append(File.separator);
		}
		
		/* add .class extension*/
		filename.append(classFilePath);
		filename.append(".class");

		File classFile = new File(filename.toString());

		/* read the file if it exists */
		if (classFile.exists()) {
			if( log.isDebugEnabled() ) {
				log.debug( classname + " found in " + folder + " (" + filename + ")");
			}
			try {
				/* open streams */
				ByteArrayOutputStream bout = new ByteArrayOutputStream();				
				FileInputStream fis = new FileInputStream(classFile);
	
				/* read file and write it to byte array */
				for (int readBytes = fis.read(); readBytes != -1; readBytes = fis
						.read()) {
					bout.write(readBytes);
				}
				
				/* close file stream and get bytecode */
				fis.close();
				bytecode = bout.toByteArray();
			} catch( IOException ioe ) {
				/* log and re-throw i/o errors */
				log.warn( "I/O Error occured reading file " + filename + ": " + ioe.getMessage());
				throw new BytecodeRetrievalException("Unable to retrieve bytecode for \'" + 
						classname +"\', from the file \'" + filename + "\': " + ioe.getMessage(), ioe);
			}			
		}  else {	
			if( log.isDebugEnabled() ) {
				log.debug( classname + " not found in " + folder + " (" + filename + ")");
			}
		}
		
		return bytecode;
	}

	/**
	 * Searches for <code>classname</code> in the specified jar-file and
	 * returns an bytearray representing the bytecode for that class if found.
	 * 
	 * @param pathToJarFile
	 *            jar-file to search
	 * @param classname
	 *            name of class to search for
	 * @return bytecode for the specified class, null if the jarfile don't contain that class
	 * @throws BytecodeRetrievalException if any i/o errors occur (wrong file permissions or 
	 * 			jarfile doesn't exist)
	 */
	public byte[] getByteCodeFromJarFile(String classname, File pathToJarFile) 
	throws BytecodeRetrievalException{
		byte[] bytecode = null;
		try {
			/* Open jarfile ang get file entry */
			JarFile jarFile = new JarFile(pathToJarFile);		
			String filename = classname.replace('.', '/') + ".class";
			ZipEntry fileEntry = jarFile.getEntry(filename);
			
			/* read file from zip-file if found */
			ByteArrayOutputStream fileBytes = new ByteArrayOutputStream();		
			if (fileEntry != null) {
				InputStream is = jarFile.getInputStream(fileEntry);
				for (int readBytes = is.read(); readBytes != -1; readBytes = is.read()) {
					fileBytes.write(readBytes);
				}
				is.close();
				bytecode = fileBytes.toByteArray();
			}			
		} catch( IOException ioe ) {
			throw new BytecodeRetrievalException( "Error reading class \'" + classname + "\' from " + pathToJarFile.getAbsolutePath(), ioe);
		}
		
		return bytecode;
	}

	/**
	 * Searches through entire classpath to find <code>classname</code>. If found
	 * its bytecode will be returned, otherwise <code>null</code>.
	 * 
	 * @param classname
	 * @param filename
	 * @return bytecode for the given class if found, null if the class cannot be found.
	 * @throws BytecodeRetrievalException if bycode was not found
	 */
	public byte[] getByteCodeFromClasspath(String classname)
			throws BytecodeRetrievalException {
		byte[] bytecode = new byte[0];
		
		Iterator<ClasspathEntry> cpEntries = getClasspathEntries().iterator();

		
		
		// iterate though all classpath entries. break when bytecode is
		// found or when entire classpath has been scanned.
		while (cpEntries.hasNext() && ( bytecode == null || bytecode.length == 0 ) ) {
			ClasspathEntry classpathEntry = cpEntries.next();			
			
			// get bytecode from the classpath entry
			bytecode = findBytecodeFromClasspathEntry(classname, classpathEntry);
			
			if( log.isDebugEnabled() && ( bytecode == null || bytecode.length == 0 )) {
				log.debug( classname + " not found in: " + classpathEntry);
			}
			
		}
		return bytecode;
	}

	/**
	 * Extracts <code>classname</code>'s bytecode from <code>classpathEntry</code> 
	 * @param classname the class to get
	 * @param classpathEntry where to look
	 * @return bytecode
	 */
	public byte[] findBytecodeFromClasspathEntry(String classname,
			ClasspathEntry classpathEntry) {
		byte[] bytecode = null;
		try {
			
			/* check if classpath-entry is a jar-file or a folder */
			if (classpathEntry.isDirectory()) {	
				/* search for class in folder */
				bytecode = getByteCodeFromFolder(classname, classpathEntry);

			} else if( classpathEntry.isJarFile()) {
				/* search for class in jar file */
				bytecode = getByteCodeFromJarFile(classname, classpathEntry);	
			}
			
		} catch( BytecodeRetrievalException bre ) {				
			// only log this exception
			log.warn( "Error occured while searching for \'" + classname + "\' in " + classpathEntry );
			
		}
		return bytecode;
	}

	/**
	 * Returns a list of <code>ClassPathEntry</code> objects, one for each classpath entry
	 * in system property "java.class.path"
	 * @return
	 */
	public Set<ClasspathEntry> getClasspathEntries() {
		if( cpEntries == null ) {
			cpEntries = new HashSet<ClasspathEntry>();
			String javaClasspath = System.getProperty("java.class.path");
			
			StringTokenizer cpTokens = new StringTokenizer(javaClasspath, File.pathSeparator);
			
			while( cpTokens.hasMoreTokens() ) {
				ClasspathEntry cpEntry = new ClasspathEntry( cpTokens.nextToken() );
				
				if( cpEntry.exists() ) {
					cpEntries.add( cpEntry );
					if( log.isDebugEnabled() ) {
						log.debug( "Added classpath entry: " + cpEntry );
					}
				} else  {
					log.warn( "Invalid classpath entry ignored: " + cpEntry );
				}
			}
			if( cpEntries.size() == 0 ) {				
				log.warn( "no classpath entries found (system property \"java.class.path\")");				
			}

			if( log.isInfoEnabled()) {
				log.info( cpEntries.size() + " classpath entries found: " + cpEntries );
			}
		}		
		return cpEntries;
	}
}
